xtask\tasks\fmt\house_rules/
copyright.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use anyhow::anyhow;
5use fs_err::File;
6use std::io::BufRead;
7use std::io::BufReader;
8use std::io::Read;
9use std::io::Write;
10use std::path::Path;
11
12fn commit(source: File, target: &Path) -> std::io::Result<()> {
13    source.set_permissions(target.metadata()?.permissions())?;
14    let (file, path) = source.into_parts();
15    drop(file); // Windows requires the source be closed in some cases.
16    fs_err::rename(path, target)
17}
18
19pub fn check_copyright(path: &Path, fix: bool) -> anyhow::Result<()> {
20    const HEADER_MIT_FIRST: &str = "Copyright (c) Microsoft Corporation.";
21    const HEADER_MIT_SECOND: &str = "Licensed under the MIT License.";
22
23    let ext = path
24        .extension()
25        .and_then(|e| e.to_str())
26        .unwrap_or_default();
27
28    if !matches!(
29        ext,
30        "rs" | "c" | "proto" | "toml" | "ts" | "js" | "py" | "ps1"
31    ) {
32        return Ok(());
33    }
34
35    let f = BufReader::new(File::open(path)?);
36    let mut lines = f.lines();
37    let (script_interpreter_line, blank_after_script_interpreter_line, first_content_line) = {
38        let line = lines.next().unwrap_or(Ok(String::new()))?;
39        // Besides the "py", "ps1, "toml", and "config" files, only for Rust,
40        // `#!` is in the first set of the grammar. That's why we need to check
41        // the extension for not being "rs".
42        // Someone may decide to put a script interpreter line (aka "shebang")
43        // in a .config or a .toml file, and mark the file as executable. While
44        // that's not common, we choose not to constrain creativity.
45        if line.starts_with("#!") && ext != "rs" {
46            let script_interpreter_line = line;
47            let after_script_interpreter_line = lines.next().unwrap_or(Ok(String::new()))?;
48            (
49                Some(script_interpreter_line),
50                Some(after_script_interpreter_line.is_empty()),
51                lines.next().unwrap_or(Ok(String::new()))?,
52            )
53        } else {
54            (None, None, line)
55        }
56    };
57    let second_content_line = lines.next().unwrap_or(Ok(String::new()))?;
58    let third_content_line = lines.next().unwrap_or(Ok(String::new()))?;
59
60    // Preserve any files which are copyright, but not by Microsoft.
61    if first_content_line.contains("Copyright") && !first_content_line.contains("Microsoft") {
62        return Ok(());
63    }
64
65    let mut missing_banner = !first_content_line.contains(HEADER_MIT_FIRST)
66        || !second_content_line.contains(HEADER_MIT_SECOND);
67    let mut missing_blank_line = !third_content_line.is_empty();
68    let mut header_lines = 2;
69
70    // TEMP: until we have more robust infrastructure for distinct
71    // microsoft-internal checks, include this "escape hatch" for preserving
72    // non-MIT licensed files when running `xtask fmt` in the msft internal
73    // repo. This uses a job-specific env var, instead of being properly plumbed
74    // through via `clap`, to make it easier to remove in the future.
75    let is_msft_internal = std::env::var("XTASK_FMT_COPYRIGHT_ALLOW_MISSING_MIT").is_ok();
76    if is_msft_internal && missing_banner {
77        // support both new and existing copyright banner styles
78        missing_banner =
79            !(first_content_line.contains("Copyright") && first_content_line.contains("Microsoft"));
80        missing_blank_line = !second_content_line.is_empty();
81        header_lines = 1;
82    }
83
84    if fix {
85        // windows gets touchy if you try and rename files while there are open
86        // file handles
87        drop(lines);
88
89        if missing_banner || missing_blank_line {
90            let path_fix = &{
91                let mut p = path.to_path_buf();
92                let ok = p.set_extension(format!("{}.fix", ext));
93                assert!(ok);
94                p
95            };
96
97            let mut f = BufReader::new(File::open(path)?);
98            let mut f_fixed = File::create(path_fix)?;
99
100            if let Some(script_interpreter_line) = &script_interpreter_line {
101                writeln!(f_fixed, "{script_interpreter_line}")?;
102                f.read_line(&mut String::new())?;
103            }
104            if let Some(blank_after_script_interpreter_line) = blank_after_script_interpreter_line {
105                if !blank_after_script_interpreter_line {
106                    writeln!(f_fixed)?;
107                }
108            }
109
110            if missing_banner {
111                let prefix = match ext {
112                    "rs" | "c" | "proto" | "ts" | "js" => "//",
113                    "toml" | "py" | "ps1" | "config" => "#",
114                    _ => unreachable!(),
115                };
116
117                // Preserve the UTF-8 BOM if it exists.
118                if script_interpreter_line.is_none() && first_content_line.starts_with('\u{feff}') {
119                    write!(f_fixed, "\u{feff}")?;
120                    // Skip the BOM.
121                    f.read_exact(&mut [0; 3])?;
122                }
123
124                writeln!(f_fixed, "{} {}", prefix, HEADER_MIT_FIRST)?;
125                if !is_msft_internal {
126                    writeln!(f_fixed, "{} {}", prefix, HEADER_MIT_SECOND)?;
127                }
128
129                writeln!(f_fixed)?; // also add that missing blank line
130            } else if missing_blank_line {
131                // copy the valid header from the current file
132                for _ in 0..header_lines {
133                    let mut s = String::new();
134                    f.read_line(&mut s)?;
135                    write!(f_fixed, "{}", s)?;
136                }
137
138                // ...but then tack on the blank newline as well
139                writeln!(f_fixed)?;
140            }
141
142            // copy over the rest of the file contents
143            std::io::copy(&mut f, &mut f_fixed)?;
144
145            // Windows gets touchy if you try and rename files while there are open
146            // file handles.
147            drop(f);
148            commit(f_fixed, path)?;
149        }
150    }
151
152    // Consider using an enum if there more than three,
153    // or the errors need to be compared.
154    let mut missing = vec![];
155    if missing_banner {
156        missing.push("the copyright & license header");
157    }
158    if missing_blank_line {
159        missing.push("a blank line after the copyright & license header");
160    }
161    if let Some(blank_after_script_interpreter_line) = blank_after_script_interpreter_line {
162        if !blank_after_script_interpreter_line {
163            missing.push("a blank line after the script interpreter line");
164        }
165    }
166
167    if missing.is_empty() {
168        return Ok(());
169    }
170
171    if fix {
172        log::info!(
173            "applied fixes for missing {:?} in {}",
174            missing,
175            path.display()
176        );
177        Ok(())
178    } else {
179        Err(anyhow!("missing {:?} in {}", missing, path.display()))
180    }
181}